package org.budo.mybatis.dao.impl;

import java.io.Serializable;
import java.sql.Connection;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSession;
import org.budo.mybatis.dao.MybatisDao;
import org.budo.mybatis.dao.statement.builder.StatementBuilderConsts;
import org.budo.mybatis.executor.keygen.BudoJdbc3KeyGenerator;
import org.budo.mybatis.session.delegate.SqlSessionDelegate;
import org.budo.support.assertion.Assert;
import org.budo.support.dao.page.Page;
import org.budo.support.lang.util.ListUtil;
import org.budo.support.lang.util.MapUtil;
import org.budo.support.lang.util.ReflectUtil;
import org.budo.support.lang.util.StringUtil;
import org.budo.support.pagehelper.PageHelperUtil;
import org.budo.support.slf4j.Slf4j;
import org.mybatis.spring.SqlSessionTemplate;
import org.slf4j.Logger;

/**
 * 推荐使用MybatisDaoFactory来获取实例
 * 
 * @author limingwei2
 * @see org.budo.mybatis.dao.factory.MybatisDaoFactory
 */
public class MybatisDaoImpl extends AbstractMybatisDao implements MybatisDao, StatementBuilderConsts {
    private static final Logger log = Slf4j.getLogger();

    /**
     * @see org.apache.ibatis.executor.keygen.KeyGenerator
     * @see org.apache.ibatis.builder.xml.XMLStatementBuilder#parseStatementNode()
     * @see org.apache.ibatis.executor.statement.PreparedStatementHandler#instantiateStatement(Connection)
     */
    public Integer insert(Class<?> entityType, Object entity) {
        this.save(entityType, entity);

        return BudoJdbc3KeyGenerator.getFirstGeneratoredKeyAsInteger();
    }

    public Integer insertIgnoreNull(Class<?> entityType, Object entity) {
        this.saveIgnoreNull(entityType, entity);

        return BudoJdbc3KeyGenerator.getFirstGeneratoredKeyAsInteger();
    }

    public Integer[] insertBatch(Class<?> entityType, List<?> list) {
        this.saveBatch(entityType, list);

        return BudoJdbc3KeyGenerator.getGeneratoredKeyAsIntegerArray();
    }

    public Boolean save(Class<?> entityType, Object entity) {
        String statementId = entityType.getName() + "." + STATEMENT_SAVE;
        Configuration configuration = this.getConfiguration();
        if (!this.getStatementBuilder().hasStatement(configuration, statementId)) {
            this.getStatementBuilder().buildSaveStatement(configuration, entityType);

            MappedStatement mappedStatement = configuration.getMappedStatement(statementId);
            ReflectUtil.setFieldValue(mappedStatement, "keyGenerator", this.getKeyGenerator());
        }

        return 0 < this.getSqlSession().insert(statementId, entity);
    }

    public Boolean saveIgnoreNull(Class<?> entityType, Object entity) {
        String statementId = entityType.getName() + "." + STATEMENT_SAVE_IGNORE_NULL;
        Configuration configuration = this.getConfiguration();
        if (!this.getStatementBuilder().hasStatement(configuration, statementId)) {
            this.getStatementBuilder().buildSaveIgnoreNullStatement(configuration, entityType);

            MappedStatement mappedStatement = configuration.getMappedStatement(statementId);
            ReflectUtil.setFieldValue(mappedStatement, "keyGenerator", this.getKeyGenerator());
        }

        return 0 < this.getSqlSession().insert(statementId, entity);
    }

    public Boolean saveBatch(Class<?> entityType, List<?> list) {
        if (null == list || list.isEmpty() || null == entityType) {
            log.warn("#90 saveBatch error, return false, entityType=" + entityType + ", list=" + list);
            return false;
        }

        String statementId = entityType.getName() + "." + STATEMENT_SAVE_BATCH;
        Configuration configuration = this.getConfiguration();
        if (!this.getStatementBuilder().hasStatement(configuration, statementId)) {
            this.getStatementBuilder().buildSaveBatchStatement(configuration, entityType);

            MappedStatement mappedStatement = configuration.getMappedStatement(statementId);
            ReflectUtil.setFieldValue(mappedStatement, "keyGenerator", this.getKeyGenerator());
        }

        return 0 < this.getSqlSession().insert(statementId, list);
    }

    @Override
    public Boolean saveBatch(Class<?> entityType, List<?> list, Integer batchSize) {
        List<?> splitList = ListUtil.splitBySize(list, batchSize);
        for (Object eachSub : splitList) {
            List<?> subList = (List<?>) eachSub;
            this.saveBatch(entityType, subList);
        }

        return true;
    }

    public <T> T findById(Class<T> entityType, Serializable id) {
        String statementId = entityType.getName() + "." + STATEMENT_FIND_BY_ID;
        Configuration configuration = this.getConfiguration();
        if (!this.getStatementBuilder().hasStatement(configuration, statementId)) {
            this.getStatementBuilder().buildFindByIdStatement(configuration, entityType);
        }

        return this.getSqlSession().selectOne(statementId, id);
    }

    public <T> T findByColumns(Class<T> entityType, Map<String, Object> columnsAndValues) {
        String statementId = entityType.getName() + "." + STATEMENT_FIND_BY_COLUMNS;
        Configuration configuration = this.getConfiguration();
        if (!this.getStatementBuilder().hasStatement(configuration, statementId)) {
            this.getStatementBuilder().buildFindByColumnsStatement(configuration, entityType);
        }

        Map<String, Object> parameter = MapUtil.stringObjectMap("columnsAndValues", columnsAndValues);
        return this.getSqlSession().selectOne(statementId, parameter);
    }

    public <T> List<T> listByColumns(Class<T> entityType, Map<String, Object> columnsAndValues, Page page) {
        PageHelperUtil.startPage(page);

        String statementId = entityType.getName() + "." + STATEMENT_LIST_BY_COLUMNS;
        Configuration configuration = this.getConfiguration();
        if (!this.getStatementBuilder().hasStatement(configuration, statementId)) {
            this.getStatementBuilder().buildListByColumnsStatement(configuration, entityType);
        }

        Map<String, Object> parameter = MapUtil.stringObjectMap("columnsAndValues", columnsAndValues);
        return this.getSqlSession().selectList(statementId, parameter);
    }

    public Integer updateColumns(Class<?> entityType, Serializable[] ids, Map<String, Object> columnsAndValues) {
        String statementId = entityType.getName() + "." + STATEMENT_UPDATE_COLUMNS;
        Configuration configuration = this.getConfiguration();
        if (!this.getStatementBuilder().hasStatement(configuration, statementId)) {
            this.getStatementBuilder().buildUpdateColumnsStatement(configuration, entityType);
        }

        Map<String, Object> parameter = MapUtil.stringObjectMap("ids", ids, "columnsAndValues", columnsAndValues);
        return this.getSqlSession().update(statementId, parameter);
    }

    public Boolean update(Class<?> entityType, Object entity) {
        Assert.notNull(entity, "MybatisDaoImpl.update, entity is null, entity=" + entity);

        String statementId = entityType.getName() + "." + STATEMENT_UPDATE;
        Configuration configuration = this.getConfiguration();
        if (!this.getStatementBuilder().hasStatement(configuration, statementId)) {
            this.getStatementBuilder().buildUpdateStatement(configuration, entityType);
        }

        return 0 < this.getSqlSession().update(statementId, entity);
    }

    public Boolean deleteById(Class<?> entityType, Serializable id) {
        String statementId = entityType.getName() + "." + STATEMENT_DELETE_BY_ID;
        Configuration configuration = this.getConfiguration();
        if (!this.getStatementBuilder().hasStatement(configuration, statementId)) {
            this.getStatementBuilder().buildDeleteByIdStatement(configuration, entityType);
        }

        return 0 < this.getSqlSession().delete(statementId, id);
    }

    public Integer deleteByIds(Class<?> entityType, Serializable[] ids) {
        String statementId = entityType.getName() + "." + STATEMENT_DELETE_BY_IDS;
        Configuration configuration = this.getConfiguration();
        if (!this.getStatementBuilder().hasStatement(configuration, statementId)) {
            this.getStatementBuilder().buildDeleteByIdsStatement(configuration, entityType);
        }

        if (null == ids) {
            return null;
        }

        if (ids.length < 1) {
            return 0;
        }

        return this.getSqlSession().delete(statementId, ids);
    }

    public Boolean updateIgnoreNull(Class<?> entityType, Object entity) {
        Assert.notNull(entity, "MybatisDaoImpl.updateIgnoreNull, entity is null, entity=" + entity);

        String statementId = entityType.getName() + "." + STATEMENT_UPDATE_IGNORE_NULL;
        Configuration configuration = this.getConfiguration();
        if (!this.getStatementBuilder().hasStatement(configuration, statementId)) {
            this.getStatementBuilder().buildUpdateIgnoreNullStatement(configuration, entityType);
        }

        return 0 < this.getSqlSession().update(statementId, entity);
    }

    public Integer deleteBySql(String sql, Map<String, Object> parameter) {
        sql = this.processArrayParameter(sql, parameter);

        String statementId = this.getStatementBuilder().sqlStatementId(sql);
        Configuration configuration = this.getConfiguration();
        if (!this.getStatementBuilder().hasStatement(configuration, statementId)) {
            this.getStatementBuilder().buildDeleteBySqlStatement(configuration, sql);
        }

        if (null == parameter) {
            return this.getSqlSession().delete(statementId);
        }

        return this.getSqlSession().delete(statementId, parameter);
    }

    public <T> T findBySql(Class<T> entityType, String sql, Map<String, Object> parameter) {
        sql = this.processArrayParameter(sql, parameter);

        String statementId = this.getStatementBuilder().sqlStatementId(sql);
        Configuration configuration = this.getConfiguration();
        if (!this.getStatementBuilder().hasStatement(configuration, statementId)) {
            this.getStatementBuilder().buildFindBySqlStatement(configuration, entityType, sql);
        }

        if (null == parameter) {
            return this.getSqlSession().selectOne(statementId);
        }

        return this.getSqlSession().selectOne(statementId, parameter);
    }

    public <T> T findByExample(Class<T> entityType, Object example) {
        Assert.notNull(example, "MybatisDaoImpl.updateIgnoreNull, example is null, example=" + example);

        String statementId = entityType.getName() + "." + STATEMENT_FIND_BY_EXAMPLE;
        Configuration configuration = this.getConfiguration();
        if (!this.getStatementBuilder().hasStatement(configuration, statementId)) {
            this.getStatementBuilder().buildFindByExampleStatement(configuration, entityType);
        }

        return this.getSqlSession().selectOne(statementId, example);
    }

    public Integer updateBySql(String sql, Map<String, Object> parameter) {
        String processedSql = this.processArrayParameter(sql, parameter);

        String statementId = this.getStatementBuilder().sqlStatementId(processedSql);
        Configuration configuration = this.getConfiguration();
        if (!this.getStatementBuilder().hasStatement(configuration, statementId)) {
            this.getStatementBuilder().buildUpdateBySqlStatement(configuration, processedSql);
        }

        if (null == parameter) {
            return this.getSqlSession().update(statementId);
        }

        return this.getSqlSession().update(statementId, parameter);
    }

    public <T> List<T> list(Class<T> entityType, Page page) {
        PageHelperUtil.startPage(page);

        String sql = "SELECT * FROM " + this.getEntityConfiger().getTableName(entityType);
        return this.listBySql(entityType, sql, new HashMap<String, Object>(), page);
    }

    public <T> List<T> listByIds(Class<T> entityType, Serializable[] ids) {
        String statementId = entityType.getName() + "." + STATEMENT_LIST_BY_IDS;
        Configuration configuration = this.getConfiguration();
        if (!this.getStatementBuilder().hasStatement(configuration, statementId)) {
            this.getStatementBuilder().buildListByIdsStatement(configuration, entityType);
        }

        if (null == ids) {
            log.error("listByIds error, return null, ids=" + ids);
            return null;
        }

        if (ids.length < 1) {
            return Collections.EMPTY_LIST;
        }

        return this.getSqlSession().selectList(statementId, ids);
    }

    public <T> List<T> listBySql(Class<T> entityType, String sql, Map<String, Object> parameter, Page page) {
        sql = this.processArrayParameter(sql, parameter);

        PageHelperUtil.startPage(page);

        String statementId = this.getStatementBuilder().sqlStatementId(sql);
        Configuration configuration = this.getConfiguration();
        if (!this.getStatementBuilder().hasStatement(configuration, statementId)) {
            this.getStatementBuilder().buildListBySqlStatement(configuration, entityType, sql);
        }

        if (null == parameter) {
            return this.getSqlSession().selectList(statementId);
        }

        return this.getSqlSession().selectList(statementId, parameter);
    }

    @Override
    public List listBySql(ResultMap resultMap, String sql, Map<String, Object> parameter, Page page) {
        sql = this.processArrayParameter(sql, parameter);

        PageHelperUtil.startPage(page);

        String statementId = this.getStatementBuilder().sqlStatementId(resultMap.getId() + "-" + sql);
        Configuration configuration = this.getConfiguration();
        if (!this.getStatementBuilder().hasStatement(configuration, statementId)) {
            this.getStatementBuilder().buildListBySqlWithResultMapIdStatement(configuration, resultMap.getId(), sql);
        }

        if (null == parameter) {
            return this.getSqlSession().selectList(statementId);
        }

        return this.getSqlSession().selectList(statementId, parameter);
    }

    public SqlSession getSqlSession() {
        if (null == super.getSqlSession() && null != super.getSqlSessionFactory()) {
            SqlSessionTemplate sqlSessionTemplate = new SqlSessionTemplate(super.getSqlSessionFactory());
            SqlSessionDelegate sqlSessionDelegate = new SqlSessionDelegate(sqlSessionTemplate, this);

            super.setSqlSession(sqlSessionDelegate);
        }
        return super.getSqlSession();
    }

    @Override
    public <T> List<T> listByStatement(String statementId, Map<String, Object> parameter, Page page) {
        PageHelperUtil.startPage(page);
        return this.getSqlSession().selectList(statementId, parameter);
    }

    public void assertResultMap(Class<?> entityType) {
        Configuration configuration = this.getConfiguration();
        this.getStatementBuilder().assertExistsResultMapReturnAllId(configuration, entityType);
    }

    /**
     * 如果Map中有数组或list类型参数，则将之转换成多个参数，并将sql中间数组占位符，改为多个元素的占位符
     */
    private String processArrayParameter(String sql, Map<String, Object> parameter) {
        if (null == sql || null == parameter || sql.isEmpty() || parameter.isEmpty()) {
            return sql;
        }

        Map<String, Object> parameterAddition = new HashMap<String, Object>();
        Set<Entry<String, Object>> entrySet = parameter.entrySet();
        for (Entry<String, Object> entry : entrySet) {
            String key = entry.getKey();
            Object value = entry.getValue();

            if (null == key || null == value) {
                continue;
            }

            String target = "#{" + key + "}";

            if (!sql.contains(target)) {
                continue;
            }

            if (value instanceof Object[] || value instanceof Collection) {
                List<String> replacement = new ArrayList<String>();
                Object[] array = value instanceof Object[] ? (Object[]) value : ((Collection) value).toArray();
                for (int i = 0; i < array.length; i++) {
                    parameterAddition.put(key + "_" + i, array[i]);
                    replacement.add("#{" + key + "_" + i + "}");
                }
                sql = sql.replace(target, StringUtil.join(replacement, ", "));

                continue;
            }
        }

        parameter.putAll(parameterAddition);
        return sql;
    }

    @Override
    public <T> T findBySql(ResultMap resultMap, String sql, Map<String, Object> parameter) {
        sql = this.processArrayParameter(sql, parameter);

        String statementId = this.getStatementBuilder().sqlStatementId(resultMap.getId() + "-" + sql);
        Configuration configuration = this.getConfiguration();
        if (!this.getStatementBuilder().hasStatement(configuration, statementId)) {
            this.getStatementBuilder().buildFindBySqlWithResultMapIdStatement(configuration, resultMap.getId(), sql);
        }

        if (null == parameter) {
            return this.getSqlSession().selectOne(statementId);
        }

        return this.getSqlSession().selectOne(statementId, parameter);
    }
}
